package data

import (
	"context"
	"geekbang-go-homework/week04/internal/data/ent"
	"geekbang-go-homework/week04/internal/data/ent/order"
	"geekbang-go-homework/week04/internal/data/ent/orderitem"
	"geekbang-go-homework/week04/internal/domain"
	e "geekbang-go-homework/week04/internal/pkg/errors"
	"sort"
	"time"

	"github.com/pkg/errors"
)

type OrderRepo struct {
	client    *ent.Client
	tx        *ent.Tx
	order     *ent.OrderClient
	orderItem *ent.OrderItemClient
}

func NewOrderRepo(client *ent.Client) domain.OrderRepo {
	return &OrderRepo{
		client:    client,
		order:     client.Order,
		orderItem: client.OrderItem,
	}
}

// Tx start transaction
func (o *OrderRepo) Tx(ctx context.Context) (domain.OrderRepo, error) {
	tx, err := o.client.Tx(ctx)
	if err != nil {
		return nil, errors.Wrapf(err, "start transaction error: %v", err)
	}
	return &OrderRepo{
		tx:        tx,
		order:     tx.Order,
		orderItem: tx.OrderItem}, nil
}

// Rollback transaction
func (o *OrderRepo) Rollback() error {
	if err := o.tx.Rollback(); err != nil {
		return errors.Wrapf(err, "rolling back transaction error: %v", err)
	}
	return nil
}

// Commit transaction
func (o *OrderRepo) Commit() error {
	if err := o.tx.Commit(); err != nil {
		return errors.Wrapf(err, "committing transaction error: %v", err)
	}
	return nil
}

// ListOrders search order list
func (o *OrderRepo) ListOrders(ctx context.Context, search *domain.SearchOrderList) (re []*domain.Order, total int64, err error) {
	query := o.client.Order.Query().WithItems()
	if search.UserId > 0 {
		query.Where(order.UserID(search.UserId))
	}
	if search.MerchantId > 0 {
		query.Where(order.MerchantID(search.MerchantId))
	}
	n, err := query.Count(ctx)
	if err != nil {
		err = errors.Wrap(err, "select order list count fail")
		return
	}
	if n == 0 {
		return
	}
	offset := int(search.PageNo-1) * int(search.PageSize)
	if offset >= n {
		return
	}
	total = int64(n)
	list, err := query.Offset(offset).Limit(int(search.PageSize)).All(ctx)
	if err != nil {
		err = errors.Wrap(err, "select order list fail")
		return
	}
	re = make([]*domain.Order, len(list))
	for k, v := range list {
		re[k] = o.poConvDo(v)
	}
	return
}

// GetOrder get order by id
func (o *OrderRepo) GetOrder(ctx context.Context, id int64) (do *domain.Order, err error) {
	po, err := o.order.Query().WithItems().Where(order.ID(id)).Only(ctx)
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, e.NotFoundError()
		}
		return nil, errors.Wrap(err, "select order fail")
	}
	return o.poConvDo(po), nil
}

// CreateOrder create order
func (o *OrderRepo) CreateOrder(ctx context.Context, do *domain.Order) (re *domain.Order, err error) {
	err = WithTx(ctx, o.client, o.tx, func(tx *ent.Tx) error {
		now := time.Now()
		po, err := tx.Order.Create().SetUserID(do.UserId).
			SetMerchantID(do.MerchantId).SetPrice(do.Price).SetStatus(int8(do.Status)).
			SetCreateTime(now).SetUpdateTime(now).Save(ctx)
		if err != nil {
			return errors.Wrap(err, "save order fail")
		}
		builders := make([]*ent.OrderItemCreate, len(do.Items))
		for k, v := range do.Items {
			builders[k] = tx.OrderItem.Create().SetOrderID(po.ID).SetPrice(v.Price).
				SetItemID(v.ItemId).SetItemName(v.ItemName).SetNumber(v.Number).
				SetCreateTime(now).SetUpdateTime(now)
		}
		items, err := tx.OrderItem.CreateBulk(builders...).Save(ctx)
		if err != nil {
			return errors.Wrap(err, "save order_item fail")
		}
		po.Edges.Items = items
		re = o.poConvDo(po)
		return nil
	})
	return
}

// UpdateOrder update order
func (o *OrderRepo) UpdateOrder(ctx context.Context, do *domain.Order, mask []string) (re *domain.Order, err error) {
	re, err = o.GetOrder(ctx, do.Id)
	if err != nil {
		return
	}
	sort.Strings(mask)
	if i := sort.SearchStrings(mask, "status"); i < len(mask) && mask[i] == "status" {
		po, err := o.order.UpdateOneID(do.Id).SetStatus(int8(do.Status)).Save(ctx)
		if err != nil {
			return nil, errors.Wrap(err, "update order fail")
		}
		return o.poConvDo(po), nil
	}
	return
}

// DeleteOrder delete order
func (o *OrderRepo) DeleteOrder(ctx context.Context, id int64) error {
	return WithTx(ctx, o.client, o.tx, func(tx *ent.Tx) error {
		if err := tx.Order.DeleteOneID(1).Exec(ctx); err != nil {
			if ent.IsNotFound(err) {
				return e.NotFoundError()
			}
			return errors.Wrap(err, "delete order fail")
		}
		n, err := tx.OrderItem.Delete().Where(orderitem.OrderID(id)).Exec(ctx)
		if err != nil {
			return errors.Wrap(err, "delete order_item fail")
		}
		if n < 1 {
			return errors.New("delete order_item zero")
		}
		return nil
	})
}

func (o *OrderRepo) poConvDo(po *ent.Order) *domain.Order {
	do := &domain.Order{
		Id:         po.ID,
		UserId:     po.UserID,
		MerchantId: po.MerchantID,
		Price:      po.Price,
		Status:     domain.OrderStatus(po.Status),
		CreateTime: po.CreateTime,
		UpdateTime: po.UpdateTime,
	}
	if len(po.Edges.Items) > 0 {
		do.Items = make([]*domain.OrderItem, len(po.Edges.Items))
		for k, item := range po.Edges.Items {
			do.Items[k] = &domain.OrderItem{
				Id:       item.ID,
				ItemId:   item.ItemID,
				ItemName: item.ItemName,
				Number:   item.Number,
				Price:    item.Price,
			}
		}
	}
	return do
}
